{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "846b4bd7",
   "metadata": {},
   "source": [
    "# 요약 (Summarization)\n",
    "\n",
    "이번 튜토리얼은 문서 요약을 수행하는 방법에 대해 살펴보겠습니다.\n",
    "\n",
    "아래는 튜토리얼의 주요 개요입니다.\n",
    "\n",
    "- Stuff: 전체 문서 한 번에 요약\n",
    "- Map-Reduce: 분할 요약 후 일괄 병합\n",
    "- Map-Refine: 분할 요약 후 점진적인 병합\n",
    "- Chain of Density: N번 반복 실행하며, 누락된 entity를 보완하며 요약 개선\n",
    "- Clustering-Map-Refine: 문서의 Chunk 를 N 개의 클러스터로 나누고, 각 클러스터에서 중심점에 가까운 문서에 대한 요약을 Refine 요약."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb767264",
   "metadata": {},
   "source": [
    "## 대표적으로 알려진 요약 방식\n",
    "\n",
    "요약기를 구축할 때 중심적인 질문은 문서를 LLM의 컨텍스트 창에 어떻게 전달할 것인가입니다. 이를 위한 몇 가지 알려진 방식은 다음과 같습니다.\n",
    "\n",
    "1. `Stuff`: 단순히 모든 문서를 단일 프롬프트로 \"넣는\" 방식입니다. 이는 가장 간단한 접근 방식입니다.\n",
    "\n",
    "2. `Map-reduce`: 각 문서를 \"map\" 단계에서 개별적으로 요약한 다음, \"reduce\" 단계에서 요약본들을 최종 요약본으로 합치는 방식입니다.\n",
    "\n",
    "3. `Refine`: 입력 문서를 순회하며 반복적으로 답변을 업데이트하여 응답을 구성합니다. 각 문서에 대해, 모든 비문서 입력, 현재 문서, 그리고 최신 중간 답변을 chain에 전달하여 새로운 답변을 얻습니다.\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d041c4b",
   "metadata": {},
   "source": [
    "**실습에 활용한 문서**\n",
    "\n",
    "소프트웨어정책연구소(SPRi) - 2023년 12월호\n",
    "\n",
    "- 저자: 유재흥(AI정책연구실 책임연구원), 이지수(AI정책연구실 위촉연구원)\n",
    "- 링크: https://spri.kr/posts/view/23669\n",
    "- 파일명: `SPRI_AI_Brief_2023년12월호_F.pdf`\n",
    "\n",
    "`data` 폴더에 넣어주세요!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9f51304f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API KEY를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API KEY 정보로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0328a8af",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"Summary\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "231e2e5f",
   "metadata": {},
   "source": [
    "## Stuff\n",
    "\n",
    "`stuff documents chain`(\"stuff\"는 \"채우다\" 또는 \"채우기 위해\"의 의미)는 문서 체인 중 가장 간단한 방식입니다. 문서 목록을 가져와서 모두 프롬프트에 삽입한 다음, 그 프롬프트를 LLM에 전달합니다.\n",
    "\n",
    "이 체인은 문서가 작고 대부분의 호출에 몇 개만 전달되는 애플리케이션에 적합합니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e911836c",
   "metadata": {},
   "source": [
    "데이터를 로드합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2ce28f89",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import TextLoader\n",
    "\n",
    "# 뉴스데이터 로드\n",
    "loader = TextLoader(\"data/news.txt\")\n",
    "docs = loader.load()\n",
    "print(f\"총 글자수: {len(docs[0].page_content)}\")\n",
    "print(\"\\n========= 앞부분 미리보기 =========\\n\")\n",
    "print(docs[0].page_content[:500])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "db46d1cc",
   "metadata": {},
   "source": [
    "아래는 한국어로 요약을 작성하라는 문구가 추가된 prompt 입니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c6dcf878",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain import hub\n",
    "\n",
    "prompt = hub.pull(\"teddynote/summary-stuff-documents-korean\")\n",
    "prompt.pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "11e58ba7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# from langchain_core.prompts import PromptTemplate\n",
    "\n",
    "# prompt = PromptTemplate.from_template(\n",
    "#     \"\"\"Please summarize the sentence according to the following REQUEST.\n",
    "# REQUEST:\n",
    "# 1. Summarize the main points in bullet points in KOREAN.\n",
    "# 2. Each summarized sentence must start with an emoji that fits the meaning of the each sentence.\n",
    "# 3. Use various emojis to make the summary more interesting.\n",
    "# 4. Translate the summary into KOREAN if it is written in ENGLISH.\n",
    "# 5. DO NOT translate any technical terms.\n",
    "# 6. DO NOT include any unnecessary information.\n",
    "\n",
    "# CONTEXT:\n",
    "# {context}\n",
    "\n",
    "# SUMMARY:\"\n",
    "# \"\"\"\n",
    "# )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cebce2c0",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "from langchain.chains.combine_documents import create_stuff_documents_chain\n",
    "from langchain_teddynote.callbacks import StreamingCallback\n",
    "\n",
    "\n",
    "llm = ChatOpenAI(\n",
    "    model_name=\"gpt-4o-mini\",\n",
    "    streaming=True,\n",
    "    temperature=0,\n",
    "    callbacks=[StreamingCallback()],\n",
    ")\n",
    "\n",
    "\n",
    "stuff_chain = create_stuff_documents_chain(llm, prompt)\n",
    "answer = stuff_chain.invoke({\"context\": docs})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "af0d63d3",
   "metadata": {},
   "source": [
    "## Map-Reduce\n",
    "\n",
    "Map-reduce 방식의 요약은 긴 문서를 효율적으로 요약하는 기법입니다. \n",
    "\n",
    "이 방법은 먼저 문서를 작은 chunk로 나누는 \"map\" 단계와, 각 chunk의 요약을 결합하는 \"reduce\" 단계로 구성됩니다. \n",
    "\n",
    "1. Map 단계에서는 각 chunk를 병렬로 요약하고\n",
    "2. reduce 단계에서는 이 요약들을 하나의 최종 요약으로 통합합니다. \n",
    "\n",
    "이 접근법은 대규모 문서를 처리할 때 특히 유용하며, 언어 모델의 토큰 제한을 우회할 수 있게 해줍니다.\n",
    "\n",
    "![](./images/summarization_use_case_2.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2f6f0cce",
   "metadata": {},
   "source": [
    "데이터를 로드합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e796269b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import PyPDFLoader\n",
    "\n",
    "loader = PyPDFLoader(\"data/SPRI_AI_Brief_2023년12월호_F.pdf\")\n",
    "docs = loader.load()\n",
    "docs = docs[3:8]  # 여기서 문서의 일부만 요약\n",
    "print(f\"총 페이지수: {len(docs)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0a790030",
   "metadata": {},
   "source": [
    "### Map\n",
    "\n",
    "map 단계에서는 각 Chunk 에 대한 요약을 생성합니다. \n",
    "\n",
    "(사실 정석은 Chunk 에 대한 요약 생성이지만, 저는 핵심 내용 추출로 변경하여 진행합니다. 어차피 reduce 단계에서 요약을 하나로 합치는 과정이기 때문에 상관없습니다.)\n",
    "\n",
    "저는 이 방식이 더 유효하다고 생각하였지만, map 단계에 요약을 할지 혹은 핵심 내용을 추출할지는 본인의 판단하에 변경하여 진행할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f665b4e7",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain import hub\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "\n",
    "llm = ChatOpenAI(\n",
    "    temperature=0,\n",
    "    model_name=\"gpt-4o-mini\",\n",
    ")\n",
    "\n",
    "# map prompt 다운로드\n",
    "map_prompt = hub.pull(\"teddynote/map-prompt\")\n",
    "\n",
    "# 프롬프트 출력\n",
    "map_prompt.pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a0a45e03",
   "metadata": {},
   "source": [
    "map_chain 을 생성합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d7e8200d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# map chain 생성\n",
    "map_chain = map_prompt | llm | StrOutputParser()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "52f07bcb",
   "metadata": {},
   "source": [
    "batch() 를 호출하여 각 문서에 대한 요약본을 생성합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "98913c63",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 문서에 대한 주요내용 추출\n",
    "doc_summaries = map_chain.batch(docs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "935f4c4b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 요약된 문서의 수 출력\n",
    "len(doc_summaries)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "76317f3a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 일부 문서의 요약 출력\n",
    "print(doc_summaries[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f2bf1fe4",
   "metadata": {},
   "source": [
    "### Reduce\n",
    "\n",
    "Reduce 단계에서는 map 단계에서 진행한 핵심 내용들을 하나의 최종 요약으로 통합합니다. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2e52a1e4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# reduce prompt 다운로드\n",
    "reduce_prompt = hub.pull(\"teddynote/reduce-prompt\")\n",
    "\n",
    "# 프롬프트 출력\n",
    "reduce_prompt.pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3fcf997a",
   "metadata": {},
   "source": [
    "Reduce Chain 을 생성합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "01e3fe25",
   "metadata": {},
   "outputs": [],
   "source": [
    "# reduce chain 생성\n",
    "reduce_chain = reduce_prompt | llm | StrOutputParser()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3d365133",
   "metadata": {},
   "source": [
    "아래는 Reduce Chain 을 사용하여 스트리밍 출력 예시입니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "988ddc6f",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.messages import stream_response\n",
    "\n",
    "answer = reduce_chain.stream(\n",
    "    {\"doc_summaries\": \"\\n\".join(doc_summaries), \"language\": \"Korean\"}\n",
    ")\n",
    "stream_response(answer)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7c060863",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.runnables import chain\n",
    "\n",
    "\n",
    "@chain\n",
    "def map_reduce_chain(docs):\n",
    "    map_llm = ChatOpenAI(\n",
    "        temperature=0,\n",
    "        model_name=\"gpt-4o-mini\",\n",
    "    )\n",
    "\n",
    "    # map prompt 다운로드\n",
    "    map_prompt = hub.pull(\"teddynote/map-prompt\")\n",
    "\n",
    "    # map chain 생성\n",
    "    map_chain = map_prompt | map_llm | StrOutputParser()\n",
    "\n",
    "    # 첫 번째 프롬프트, ChatOpenAI, 문자열 출력 파서를 연결하여 체인을 생성합니다.\n",
    "    doc_summaries = map_chain.batch(docs)\n",
    "\n",
    "    # reduce prompt 다운로드\n",
    "    reduce_prompt = hub.pull(\"teddynote/reduce-prompt\")\n",
    "    reduce_llm = ChatOpenAI(\n",
    "        model_name=\"gpt-4o\",\n",
    "        temperature=0,\n",
    "        callbacks=[StreamingCallback()],\n",
    "        streaming=True,\n",
    "    )\n",
    "\n",
    "    reduce_chain = reduce_prompt | reduce_llm | StrOutputParser()\n",
    "\n",
    "    return reduce_chain.invoke(\n",
    "        {\"doc_summaries\": \"\\n\".join(doc_summaries), \"language\": \"Korean\"}\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e31de2a6",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 결과 출력\n",
    "answer = map_reduce_chain.invoke(docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "472e5d84",
   "metadata": {},
   "source": [
    "## Map-Refine\n",
    "\n",
    "Map-refine 방식은 문서 요약을 위한 또 다른 접근법으로, map-reduce와 유사하지만 약간의 차이가 있습니다. \n",
    "\n",
    "1. Map 단계: 문서를 여러 개의 작은 chunk로 나누고, 각 chunk에 대해 개별적으로 요약을 생성합니다.\n",
    "\n",
    "2. Refine 단계: 생성된 요약들을 순차적으로 처리하며 최종 요약을 점진적으로 개선합니다. 각 단계에서 이전 요약과 새로운 chunk의 정보를 결합하여 요약을 갱신합니다.\n",
    "   \n",
    "3. 반복 과정: 모든 chunk가 처리될 때까지 refine 단계를 반복합니다.\n",
    "\n",
    "4. 최종 요약: 마지막 chunk까지 처리한 후 얻은 요약이 최종 결과가 됩니다.\n",
    "\n",
    "map-refine 방식의 장점은 문서의 순서를 유지하면서 점진적으로 요약을 개선할 수 있다는 것입니다. 이는 특히 문서의 맥락이 중요한 경우에 유용할 수 있습니다. 그러나 이 방식은 map-reduce 에 비해 순차적으로 처리되기 때문에 병렬화가 어려워 대규모 문서 처리 시 시간이 더 오래 걸릴 수 있습니다.\n",
    "\n",
    "![](./images/summarization_use_case_3.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "33c83b1d",
   "metadata": {},
   "source": [
    "### Map\n",
    "\n",
    "map 단계에서는 각 Chunk 에 대한 요약을 생성합니다. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4e70abc7",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain import hub\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "\n",
    "# map llm 생성\n",
    "map_llm = ChatOpenAI(\n",
    "    temperature=0,\n",
    "    model_name=\"gpt-4o-mini\",\n",
    ")\n",
    "\n",
    "# map chain 생성\n",
    "map_summary = hub.pull(\"teddynote/map-summary-prompt\")\n",
    "\n",
    "# 프롬프트 출력\n",
    "map_summary.pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d9cbde9",
   "metadata": {},
   "source": [
    "map_chain 을 생성합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dc1f58db",
   "metadata": {},
   "outputs": [],
   "source": [
    "# map chain 생성\n",
    "map_chain = map_summary | llm | StrOutputParser()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b6f61f4e",
   "metadata": {},
   "source": [
    "첫 번째 문서에 대한 요약본을 출력합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6220830c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 첫 번째 문서의 요약 출력\n",
    "print(map_chain.invoke({\"documents\": docs[0], \"language\": \"Korean\"}))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e6804299",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 모든 문서를 입력으로 정의합니다.\n",
    "input_doc = [{\"documents\": doc, \"language\": \"Korean\"} for doc in docs]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ff58b7d1",
   "metadata": {},
   "outputs": [],
   "source": [
    "input_doc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "22e847c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 모든 문서에 대한 요약본을 출력합니다.\n",
    "print(map_chain.batch(input_doc))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5ac15984",
   "metadata": {},
   "source": [
    "### Refine\n",
    "\n",
    "Refine 단계에서는 이전의 map 단계에서 생성한 chunk들을 순차적으로 처리하며 최종 요약을 점진적으로 개선합니다. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8679bd44",
   "metadata": {},
   "outputs": [],
   "source": [
    "# refine prompt 다운로드\n",
    "refine_prompt = hub.pull(\"teddynote/refine-prompt\")\n",
    "\n",
    "# 프롬프트 출력\n",
    "refine_prompt.pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "97282d5f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# refine llm 생성\n",
    "refine_llm = ChatOpenAI(\n",
    "    temperature=0,\n",
    "    model_name=\"gpt-4o-mini\",\n",
    ")\n",
    "\n",
    "# refine chain 생성\n",
    "refine_chain = refine_prompt | refine_llm | StrOutputParser()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c09e3467",
   "metadata": {},
   "source": [
    "아래는 map_reduce_chain 을 생성하는 예시입니다. \n",
    "\n",
    "지금까지의 일련의 과정을 하나의 chain 으로 엮습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2bcb389f",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.runnables import chain\n",
    "\n",
    "\n",
    "@chain\n",
    "def map_refine_chain(docs):\n",
    "\n",
    "    # map chain 생성\n",
    "    map_summary = hub.pull(\"teddynote/map-summary-prompt\")\n",
    "\n",
    "    map_chain = (\n",
    "        map_summary\n",
    "        | ChatOpenAI(\n",
    "            model_name=\"gpt-4o-mini\",\n",
    "            temperature=0,\n",
    "        )\n",
    "        | StrOutputParser()\n",
    "    )\n",
    "\n",
    "    input_doc = [{\"documents\": doc.page_content, \"language\": \"Korean\"} for doc in docs]\n",
    "\n",
    "    # 첫 번째 프롬프트, ChatOpenAI, 문자열 출력 파서를 연결하여 체인을 생성합니다.\n",
    "    doc_summaries = map_chain.batch(input_doc)\n",
    "\n",
    "    refine_prompt = hub.pull(\"teddynote/refine-prompt\")\n",
    "\n",
    "    refine_llm = ChatOpenAI(\n",
    "        model_name=\"gpt-4o-mini\",\n",
    "        temperature=0,\n",
    "        callbacks=[StreamingCallback()],\n",
    "        streaming=True,\n",
    "    )\n",
    "\n",
    "    refine_chain = refine_prompt | refine_llm | StrOutputParser()\n",
    "\n",
    "    previous_summary = doc_summaries[0]\n",
    "\n",
    "    for current_summary in doc_summaries[1:]:\n",
    "\n",
    "        previous_summary = refine_chain.invoke(\n",
    "            {\n",
    "                \"previous_summary\": previous_summary,\n",
    "                \"current_summary\": current_summary,\n",
    "                \"language\": \"Korean\",\n",
    "            }\n",
    "        )\n",
    "        print(\"\\n\\n-----------------\\n\\n\")\n",
    "\n",
    "    return previous_summary"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "265fc54d",
   "metadata": {},
   "outputs": [],
   "source": [
    "refined_summary = map_refine_chain.invoke(docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd0e2068",
   "metadata": {},
   "source": [
    "## Chain of Density\n",
    "\n",
    "- 논문: https://arxiv.org/pdf/2309.04269\n",
    "\n",
    "\"Chain of Density\" (CoD) 프롬프트는 GPT-4를 사용한 요약 생성을 개선하기 위해 개발된 기법입니다. \n",
    "\n",
    "이 방법은 초기에 개체가 적은 요약을 생성한 후, 길이를 늘리지 않으면서 누락된 중요 개체들을 반복적으로 통합하는 과정을 거칩니다. 연구 결과, CoD로 생성된 요약은 일반 프롬프트보다 더 추상적이고 정보 융합이 뛰어나며, 인간이 작성한 요약과 비슷한 밀도를 가진 것으로 나타났습니다.\n",
    "\n",
    "1. 점진적 개선: CoD는 초기에 개체가 적은 간단한 요약을 생성한 후, 단계적으로 중요한 개체들을 추가하며 요약을 개선합니다. 이 과정에서 요약의 길이는 유지되면서 정보 밀도가 증가하여 읽기 쉬우면서도 정보량이 풍부한 요약이 만들어집니다.\n",
    "\n",
    "2. 정보 밀도와 가독성의 균형: CoD 방식은 요약의 정보 밀도를 조절하여 정보성과 가독성 사이의 최적 균형점을 찾습니다. 연구 결과에 따르면, 사람들은 일반적인 GPT-4 요약보다 더 밀도 있지만 사람이 작성한 요약만큼 밀도가 높지 않은 CoD 요약을 선호하는 것으로 나타났습니다.\n",
    "\n",
    "3. 추상화와 정보 융합 개선: CoD로 생성된 요약은 더 추상적이고 정보 융합이 뛰어나며, 원문의 앞부분에 치우치는 경향(lead bias)이 덜합니다. 이는 요약의 전반적인 품질과 가독성을 향상시키는 데 기여합니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "85d15ad3",
   "metadata": {},
   "source": [
    "[Chain of Density Prompt](https://smith.langchain.com/prompts/chain-of-density-prompt/4582aae0?organizationId=8c9eeb3c-2665-5405-bc50-0767fdf4ca8f)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "36d2da33",
   "metadata": {},
   "source": [
    "**입력 파라미터 설명**\n",
    "\n",
    "- `content_category`: 콘텐츠 정류(예: 기사, 동영상 녹취록, 블로그 게시물, 연구 논문). 기본값: Article\n",
    "\n",
    "- `content`: 요약할 콘텐츠\n",
    "\n",
    "- `entity_range`: 콘텐츠에서 선택하여 요약에 추가할 엔티티의 수의 범위. 기본값은 `1-3`\n",
    "\n",
    "- `max_words`: 1번 요약시, 요약에 포함할 최대 단어. 기본값은 **80** 입니다.\n",
    "\n",
    "- `iterations`: 엔티티 고밀도화 라운드 수. 총 요약은 **반복 횟수+1** 입니다. 80단어의 경우 3회 반복이 이상적입니다. 요약이 더 길면 4~5회, 그리고 `entity_range` 를 예를 들어 1~4로 변경하는 것도 도움이 될 수 있습니다. 기본값: 3."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a9b267d4",
   "metadata": {},
   "source": [
    "이 코드는 Chain of Density 프롬프트를 사용하여 텍스트 요약을 생성하는 체인을 구성합니다.\n",
    "\n",
    "첫 번째 체인은 중간 결과를 보여주고, 두 번째 체인은 최종 요약만을 추출합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "87eaa08d",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a129f2df",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Chain of Density 프롬프트 다운로드\n",
    "cod_prompt = hub.pull(\"teddynote/chain-of-density-prompt\")\n",
    "\n",
    "cod_prompt.pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1737020b",
   "metadata": {},
   "outputs": [],
   "source": [
    "import textwrap\n",
    "from langchain import hub\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.output_parsers import SimpleJsonOutputParser\n",
    "\n",
    "# {content}를 제외한 모든 입력에 대한 기본값 지정\n",
    "cod_chain_inputs = {\n",
    "    \"content\": lambda d: d.get(\"content\"),\n",
    "    \"content_category\": lambda d: d.get(\"content_category\", \"Article\"),\n",
    "    \"entity_range\": lambda d: d.get(\"entity_range\", \"1-3\"),\n",
    "    \"max_words\": lambda d: int(d.get(\"max_words\", 80)),\n",
    "    \"iterations\": lambda d: int(d.get(\"iterations\", 5)),\n",
    "}\n",
    "\n",
    "# Chain of Density 프롬프트 다운로드\n",
    "cod_prompt = hub.pull(\"teddynote/chain-of-density-prompt\")\n",
    "\n",
    "# Chain of Density 체인 생성\n",
    "cod_chain = (\n",
    "    cod_chain_inputs\n",
    "    | cod_prompt\n",
    "    | ChatOpenAI(temperature=0, model=\"gpt-4o-mini\")\n",
    "    | SimpleJsonOutputParser()\n",
    ")\n",
    "\n",
    "# 두 번째 체인 생성, 최종 요약만 추출 (스트리밍 불가능, 최종 결과가 필요함)\n",
    "cod_final_summary_chain = cod_chain | (\n",
    "    lambda output: output[-1].get(\n",
    "        \"denser_summary\", '오류: 마지막 딕셔너리에 \"denser_summary\" 키가 없습니다'\n",
    "    )\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6643d8b",
   "metadata": {},
   "source": [
    "요약할 데이터를 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c81e08f6",
   "metadata": {},
   "outputs": [],
   "source": [
    "content = docs[1].page_content\n",
    "print(content)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7566ef21",
   "metadata": {},
   "source": [
    "부분 JSON 스트리밍하기. 스트리밍된 각 청크는 새로운 접미사가 추가된 동일한 JSON 딕트 목록입니다. \n",
    "\n",
    "따라서 단순히 연결하는 것이 아니라 다음 청크가 이전 청크를 덮어쓰고 반복적으로 스트리밍을 추가하는 것처럼 보이게 하려면 `\\r` 캐리지 리턴 인쇄가 필요합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6228b665",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 결과를 저장할 빈 리스트 초기화\n",
    "results: list[dict[str, str]] = []\n",
    "\n",
    "# cod_chain을 스트리밍 모드로 실행하고 부분적인 JSON 결과를 처리\n",
    "for partial_json in cod_chain.stream(\n",
    "    {\"content\": content, \"content_category\": \"Article\"}\n",
    "):\n",
    "    # 각 반복마다 results를 업데이트\n",
    "    results = partial_json\n",
    "\n",
    "    # 현재 결과를 같은 줄에 출력 (캐리지 리턴을 사용하여 이전 출력을 덮어씀)\n",
    "    print(results, end=\"\\r\", flush=True)\n",
    "\n",
    "# 총 요약 수 계산\n",
    "total_summaries = len(results)\n",
    "print(\"\\n\")\n",
    "\n",
    "# 각 요약을 순회하며 처리\n",
    "i = 1\n",
    "for cod in results:\n",
    "    # 누락된 엔티티들을 추출하고 포맷팅\n",
    "    added_entities = \", \".join(\n",
    "        [\n",
    "            ent.strip()\n",
    "            for ent in cod.get(\n",
    "                \"missing_entities\", 'ERR: \"missing_entiies\" key not found'\n",
    "            ).split(\";\")\n",
    "        ]\n",
    "    )\n",
    "    # 더 밀도 있는 요약 추출\n",
    "    summary = cod.get(\"denser_summary\", 'ERR: missing key \"denser_summary\"')\n",
    "\n",
    "    # 요약 정보 출력 (번호, 총 개수, 추가된 엔티티)\n",
    "    print(\n",
    "        f\"### CoD Summary {i}/{total_summaries}, 추가된 엔티티(entity): {added_entities}\"\n",
    "        + \"\\n\"\n",
    "    )\n",
    "    # 요약 내용을 80자 너비로 줄바꿈하여 출력\n",
    "    print(textwrap.fill(summary, width=80) + \"\\n\")\n",
    "    i += 1\n",
    "\n",
    "print(\"\\n============== [최종 요약] =================\\n\")\n",
    "print(summary)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9ba91c03",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(summary)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "09ee272d",
   "metadata": {},
   "source": [
    "## Clustering-Map-Refine\n",
    "\n",
    "이 튜토리얼의 원 저자인 gkamradt 은 긴 문서의 요약에 대해서 흥미로운 제안을 하였습니다.\n",
    "\n",
    "배경은 다음과 같습니다.\n",
    "\n",
    "1. map-reduce 나 map-refine 방식은 모두 시간이 오래 걸리고, 비용이 많이 듬.\n",
    "2. 따라서, 문서를 몇 개(N 개)의 클러스터로 나눈 뒤, 가장 중심축에서 가까운 문서를 클러스터의 대표 문서로 인지하고, 이를 map-reduce(혹은 map-refine) 방식으로 요약하는 방식을 제안.\n",
    "\n",
    "실제로 비용도 합리적으로, 결과도 만족스럽기 때문에 원 저자의 튜토리얼의 코드를 수정하여 공유합니다.\n",
    "\n",
    "- [원 저자 및 출처 - gkamradt](https://github.com/gkamradt/langchain-tutorials/blob/main/data_generation/5%20Levels%20Of%20Summarization%20-%20Novice%20To%20Expert.ipynb)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "feb12b9e",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import PyMuPDFLoader\n",
    "\n",
    "loader = PyMuPDFLoader(\"data/SPRI_AI_Brief_2023년12월호_F.pdf\")\n",
    "docs = loader.load()\n",
    "len(docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f746a903",
   "metadata": {},
   "source": [
    "아래의 코드를 실행하면 하나의 문서로 텍스트를 합칩니다. 합치는 목적은 page 별로 구분하지 않기 위해서입니다.\n",
    "\n",
    "합쳐진 문자수는 약 28K 입니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ca715d03",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 하나의 Text 로 모든 문서를 연결합니다.\n",
    "texts = \"\\n\\n\".join([doc.page_content for doc in docs])\n",
    "len(texts)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a36a3162",
   "metadata": {},
   "source": [
    "`RecursiveCharacterTextSplitter` 를 사용하여 하나의 Text 를 여러 문서로 나눕니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a8a11656",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
    "\n",
    "text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)\n",
    "split_docs = text_splitter.split_text(texts)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c9e8fef7",
   "metadata": {},
   "source": [
    "나누어진 문서의 수를 확인합니다. 여기서는 79개의 문서로 나누었습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d7697377",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 총 문서의 수 확인\n",
    "len(split_docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "517ddb9e",
   "metadata": {},
   "source": [
    "Upstage Embeddings 모델을 사용하여 문서를 임베딩합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "84ee29a1",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_upstage import UpstageEmbeddings\n",
    "\n",
    "embeddings = UpstageEmbeddings(model=\"solar-embedding-1-large-passage\")\n",
    "\n",
    "vectors = embeddings.embed_documents(split_docs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ae69dd2e",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import OpenAIEmbeddings\n",
    "\n",
    "embeddings = OpenAIEmbeddings()\n",
    "\n",
    "vectors = embeddings.embed_documents(split_docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "287413d3",
   "metadata": {},
   "source": [
    "총 79개의 문서를 10개 클러스터로 나눕니다. 이때 `KMeans` 를 사용하여 클러스터링을 수행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fa85e13b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.cluster import KMeans\n",
    "\n",
    "# 클러스터 수를 선택하면 문서의 콘텐츠에 따라 조정할 수 있습니다.\n",
    "num_clusters = 10\n",
    "\n",
    "# Perform K-means clustering\n",
    "kmeans = KMeans(n_clusters=num_clusters, random_state=123).fit(vectors)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c193b358",
   "metadata": {},
   "source": [
    "라벨링 된 결과를 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f2ce4aec",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 결과 확인\n",
    "kmeans.labels_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dc08c847",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.manifold import TSNE\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "import numpy as np\n",
    "\n",
    "# 경고 제거\n",
    "import warnings\n",
    "\n",
    "warnings.filterwarnings(\"ignore\")\n",
    "\n",
    "# t-SNE 수행 및 2차원으로 축소\n",
    "tsne = TSNE(n_components=2, random_state=42)\n",
    "reduced_data_tsne = tsne.fit_transform(np.array(vectors))\n",
    "\n",
    "# seaborn 스타일 설정\n",
    "sns.set_style(\"white\")\n",
    "\n",
    "# 축소된 데이터 플롯\n",
    "plt.figure(figsize=(10, 8))\n",
    "sns.scatterplot(\n",
    "    x=reduced_data_tsne[:, 0],\n",
    "    y=reduced_data_tsne[:, 1],\n",
    "    hue=kmeans.labels_,\n",
    "    palette=\"deep\",\n",
    "    s=100,\n",
    ")\n",
    "plt.xlabel(\"Dimension 1\", fontsize=12)\n",
    "plt.ylabel(\"Dimension 2\", fontsize=12)\n",
    "plt.title(\"Clustered Embeddings\", fontsize=16)\n",
    "plt.legend(title=\"Cluster\", title_fontsize=12)\n",
    "\n",
    "# 배경색 설정\n",
    "plt.gcf().patch.set_facecolor(\"white\")\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce7ef9f9",
   "metadata": {},
   "source": [
    "그러면 각 cluster 의 중심점에 가장 가까운 임베딩을 찾아서 저장해야 합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "189d15db",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "# 가장 가까운 점들을 저장할 빈 리스트 생성\n",
    "closest_indices = []\n",
    "\n",
    "# 클러스터 수만큼 반복\n",
    "for i in range(num_clusters):\n",
    "\n",
    "    # 해당 클러스터 중심으로부터의 거리 목록 구하기\n",
    "    distances = np.linalg.norm(vectors - kmeans.cluster_centers_[i], axis=1)\n",
    "\n",
    "    # 가장 가까운 점의 인덱스 찾기 (argmin을 사용하여 최소 거리 찾기)\n",
    "    closest_index = np.argmin(distances)\n",
    "\n",
    "    # 해당 인덱스를 가장 가까운 인덱스 리스트에 추가\n",
    "    closest_indices.append(closest_index)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1fc00874",
   "metadata": {},
   "outputs": [],
   "source": [
    "closest_indices"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8fb6933f",
   "metadata": {},
   "source": [
    "문서의 요약을 순서대로 진행하기 위하여 오름차순 정렬합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3b23e407",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 문서의 요약을 순서대로 진행하기 위하여 오름차순 정렬\n",
    "selected_indices = sorted(closest_indices)\n",
    "selected_indices"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "30262acd",
   "metadata": {},
   "source": [
    "10개의 선택된 문서를 출력합니다. 이 과정에서 `Document` 객체를 사용하여 문서를 생성합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "13828899",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.documents import Document\n",
    "\n",
    "selected_docs = [Document(page_content=split_docs[doc]) for doc in selected_indices]\n",
    "selected_docs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "65ffc8de",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 이전에 생성한 map_refine_chain을 사용하여 요약 생성\n",
    "refined_summary = map_refine_chain.invoke(selected_docs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4846d026",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 최종 결과 출력\n",
    "print(refined_summary)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-test",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
